Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5장. 현실적인 선택

지금까지 데이터베이스 접근 방식은 다음과 같이 발전해왔다.

  • Raw Query → 직접 제어
  • Query Builder → 구조적 개선
  • ORM → 객체 기반 추상화

각 방식은 이전의 문제를 해결하기 위해 등장했지만
👉 하나의 방식이 모든 상황을 해결하지는 못한다


하나로 통일할 수 없는 이유

이론적으로는 하나의 방식으로 대부분의 쿼리를 처리할 수 있다.

특히 ORM의 경우
👉 대부분의 데이터 접근을 표현하는 것이 가능하다

하지만 실제 서비스에서는 다른 문제가 발생한다.

👉 복잡해질수록 코드가 더 어려워진다


기능 vs 현실

ORM은 내부적으로 SQL을 생성하기 때문에
기능적으로는 대부분의 쿼리를 처리할 수 있다.

하지만 중요한 차이는 따로 있다.

👉 표현 방식과 제어력


복잡한 쿼리에서의 차이

다음과 같은 요구사항을 생각해보자.

  • 사용자별 주문 수 집계
  • 특정 개수 이상 필터링
  • 주문 수 기준 정렬

SQL

SELECT u.id, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id
HAVING COUNT(o.id) >= 3
ORDER BY order_count DESC;

👉 구조가 한눈에 보인다


Query Builder

const query = knex('users as u')
  .leftJoin('orders as o', 'u.id', 'o.user_id')
  .groupBy('u.id');

if (minOrderCount) {
  query.havingRaw('COUNT(o.id) >= ?', [minOrderCount]);
}

if (sortBy === 'orderCount') {
  query.orderByRaw('COUNT(o.id) DESC');
}

const result = await query.select('u.*');

👉 SQL 구조를 유지하면서 코드로 제어 가능


ORM (Prisma)

const users = await prisma.user.groupBy({
  by: ['id'],
  _count: {
    orders: true
  },
  having: {
    orders: {
      _count: {
        gte: 3
      }
    }
  },
  orderBy: {
    _count: {
      orders: 'desc'
    }
  }
});

무엇이 문제인가

ORM도 동일한 기능을 표현할 수 있다.
하지만 코드의 특성이 달라진다.

  • SQL 구조가 직접적으로 보이지 않는다
  • ORM 문법을 따로 이해해야 한다
  • 복잡해질수록 가독성이 떨어진다

👉 즉, 표현은 가능하지만 이해 비용이 증가한다


성능 제어의 한계

ORM은 편리하지만
실행되는 SQL에 대한 제어가 제한된다.

예를 들어

  • 어떤 JOIN 방식이 사용되는지
  • 어떤 인덱스가 선택되는지
  • 쿼리가 몇 번 실행되는지

를 코드만 보고 파악하기 어렵다.

👉 특히 N+1 문제처럼
👉 의도하지 않은 쿼리 증가가 발생할 수 있다

또한

  • index hint
  • 실행 계획 제어
  • 특정 DB 기능 활용

과 같은 세밀한 튜닝은 어렵거나 제한적이다.


결국 다시 SQL로 돌아간다

실무에서는 다음과 같은 흐름이 자주 발생한다.

  • ORM으로 구현은 가능하지만 코드가 복잡해짐
  • 성능 문제가 발생함
  • 실행되는 SQL을 직접 제어해야 하는 상황 발생

이 시점에서 개발자는 자연스럽게 선택한다.

👉 “이건 그냥 SQL로 쓰는 게 낫다”


그래서 등장하는 현실적인 구조

이러한 이유로 실제 서비스에서는
하나의 방식이 아니라 여러 방식을 함께 사용한다.

👉 혼합 전략


역할에 따른 선택

각 방식은 서로 다른 역할을 가진다.

일반적인 데이터 처리 → ORM

const user = await prisma.user.findUnique({
  where: { id: userId }
});
  • 코드가 간결하다
  • 생산성이 높다
  • 구조적으로 관리하기 쉽다

👉 대부분의 CRUD 작업에 적합하다


복잡한 쿼리 구성 → Query Builder

const query = knex('users as u')
  .leftJoin('orders as o', 'u.id', 'o.user_id')
  .groupBy('u.id');

if (minOrderCount) {
  query.havingRaw('COUNT(o.id) >= ?', [minOrderCount]);
}

if (sortBy === 'orderCount') {
  query.orderByRaw('COUNT(o.id) DESC');
}
  • SQL 구조를 유지하면서 코드로 제어 가능
  • 동적인 쿼리 구성에 유리
  • 복잡한 조건과 구조 처리에 적합

성능 / 특수 기능 → Raw Query

const result = await db.query(`
  SELECT u.id, COUNT(o.id) as order_count
  FROM users u
  LEFT JOIN orders o ON u.id = o.user_id
  GROUP BY u.id
`);
  • 데이터베이스 기능을 그대로 활용 가능
  • 성능 튜닝에 유리
  • 복잡한 쿼리 표현에 가장 강력

핵심은 역할이다

이 세 가지 방식은 서로 경쟁 관계가 아니다.

👉 각각의 역할이 다르다

  • ORM → 생산성
  • Query Builder → 유연성
  • Raw Query → 제어력

잘못된 접근 방식

다음과 같은 접근은 오히려 문제를 만든다.

  • “ORM만 사용해야 한다”
  • “Raw Query는 나쁜 방식이다”
  • “하나로 통일해야 한다”

이런 선택은

👉 특정 상황에서 비효율적인 구조를 만들게 된다


선택 기준

실제 선택은 다음 기준으로 이루어진다.

  • 쿼리의 복잡도
  • 성능 요구사항
  • 유지보수 용이성
  • 팀의 이해도

정리

데이터베이스 접근 방식은
하나의 정답이 있는 문제가 아니다.

👉 중요한 것은 기술 자체가 아니라
👉 상황에 맞는 선택이다